/* @flow */
/**
 * 完整版 Vue 的 $mount 函数要做的核心事情就是编译模板(template)字符串为渲染函数，并将渲染函数赋值给 vm.$options.render 选项，
 * 这个选项将会在真正挂载组件的 mountComponent 函数中
 */
import config from 'core/config'
import { warn, cached } from 'core/util/index'
import { mark, measure } from 'core/util/perf'

// 导入运行时的 Vue
import Vue from './runtime/index'
import { query } from './util/index'
// 从 ./compiler/index.js 文件中导入 compileToFunctions
import { compileToFunctions } from './compiler/index'
import { shouldDecodeNewlines, shouldDecodeNewlinesForHref } from './util/compat'

// 根据 id 获取元素的 innerHTML(通过 cached缓存创建来避免重复求值)
const idToTemplate = cached(id => {
  const el = query(id)
  return el && el.innerHTML
})

// 使用 mount 变量缓存运行时的 Vue.prototype.$mount 方法
const mount = Vue.prototype.$mount
// 然后重写 Vue.prototype.$mount 方法（为了给运行时版的 $mount 函数增加编译模板的能力）
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  // 如果传递了 el 参数，就使用 query 函数获取到指定的 DOM 元素并重新赋值给 el(挂载点) 变量
  el = el && query(el)

  /* body或html不允许作为挂载点，因为挂载点会被替换掉 */
  if (el === document.body || el === document.documentElement) {
    process.env.NODE_ENV !== 'production' && warn(
      `Do not mount Vue to <html> or <body> - mount to normal elements instead.`
    )
    return this
  }

  const options = this.$options
  // 渲染函数存在时才能进行挂载操作，不存在时则使用 template 或 el 选项构建渲染函数
  if (!options.render) {
    let template = options.template
    // 如果template存在则优先尝试将 template 编译成渲染函数
    if (template) {
      // 如果template是字符串
      if (typeof template === 'string') {
        // 第一个字符是 #则把该字符串作为 css 选择符去选中对应的元素，并把该元素的 innerHTML 作为模板
        if (template.charAt(0) === '#') {
          template = idToTemplate(template)
          /* istanbul ignore if */
          if (process.env.NODE_ENV !== 'production' && !template) {
            warn(
              `Template element not found or is empty: ${options.template}`,
              this
            )
          }
        }
      // 如果template是元素节点(nodeType存在)，则使用该元素的 innerHTML 作为模板
      } else if (template.nodeType) {
        template = template.innerHTML
      // 若 template 既不是字符串又不是元素节点，那么在非生产环境会提示开发者传递的 template 选项无效  
      } else {
        if (process.env.NODE_ENV !== 'production') {
          warn('invalid template option:' + template, this)
        }
        return this
      }
    // template不存在时再检测el是否存在，el 存在的话则使用 el.outerHTML 作为 template 的值 
    } else if (el) {
      template = getOuterHTML(el)
    }
    // 经过上面的处理，template应该为一个字符串类型的值，但可能为空字符串，所以继续处理
    if (template) {
      /* istanbul ignore if */
      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
        // 添加mark标识，用来统计编译器性能
        mark('compile')
      }
      // 使用 compileToFunctions 函数将模板(template)字符串编译为渲染函数(render)，
      // 并将渲染函数添加到 vm.$options 选项中(options 是 vm.$options 的引用)
      const { render, staticRenderFns } = compileToFunctions(template, {
        shouldDecodeNewlines,
        shouldDecodeNewlinesForHref,
        delimiters: options.delimiters,
        comments: options.comments
      }, this)
      options.render = render
      options.staticRenderFns = staticRenderFns

      /* istanbul ignore if */
      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
        // 添加mark标识，用来统计编译器性能
        mark('compile end')
        measure(`vue ${this._name} compile`, 'compile', 'compile end')
      }
    }
  }
  return mount.call(this, el, hydrating)
}

/**
 * 获取元素的 outerHTML(不支持 IE中的SVG元素)
 */
function getOuterHTML (el: Element): string {
  // 元素的 outerHTML 属性未必存在(IE9-11 中 SVG 标签元素是没有 innerHTML 和 outerHTML 这两个属性)
  if (el.outerHTML) {
    return el.outerHTML
  } else {
    // 把 SVG 元素放到一个新创建的 div 元素中,新 div 元素的 innerHTML 属性的值就等价于 SVG 标签 outerHTML 的值
    const container = document.createElement('div')
    container.appendChild(el.cloneNode(true))
    return container.innerHTML
  }
}
// 在Vue上添加一个全局API `Vue.compile` 其值为上面导入进来的compileToFunctions
Vue.compile = compileToFunctions

export default Vue
